import "reflect-metadata";

export interface Constructor<T extends Object> extends Function {
    new(...args): T;
    readonly prototype: Object;
}

// deprecated!, use Constructor instead.
export type Newable<T> = Constructor<T>;

@Injectable()
export class Injector {
    static get<T>(ctor: Constructor<T>): T {
        return Injector.getWithFollower(ctor, undefined);
    }

    /* Set a class as @Singleton() and give it a custom \a value for future retrieving.
     * If there exist an instance of target class, and \a overwrite haven't set,
     * setting will be failed and function will return false. Otherwise, newIt singleton
     * instance will be set and true will be returned.
     */
    static setSingleton<T>(ctor: Constructor<T>, val: T, overwrite: boolean = false): boolean {
        Injector.registerSingleton(ctor);
        if (!overwrite && ctor[Injector.singletonInstance]) {
            return false;
        }
        ctor[Injector.singletonInstance] = val;
        return true;
    }

    static isSingletonExist<T>(ctor: Constructor<T>): boolean {
        return !!ctor[Injector.singletonInstance];
    }

    private static getWithFollower<T>(ctor: Constructor<T>, fllower?: any[]): T {
        // A class without @Injectable() decorated can be instantiate too
        // **But that is not recommended**
        if (Injector.strictDecorationCheck && !ctor[Injector.isInjectable]) {
            throw new Error("To get a instance of " + ctor.name + ", you should decorate it with @Injectable().")
        }

        // There is a different instantiation for Singleton class.
        if (ctor[Injector.isSingleton]) {
            return ctor[Injector.singletonInstance] = ctor[Injector.singletonInstance] || Injector.create(ctor);
        }

        return Injector.create(ctor, fllower);
    }

    private static create<T>(ctor: Constructor<T>, follwer?: Function[]): T {
        // If there is a factory for it
        if (ctor[Injector.factorSymbol])
            return ctor[Injector.factorSymbol]();

        let argtypes = Reflect.getMetadata("design:paramtypes", ctor) || [];
        // If it depends on nothing, just newIt it.
        if (argtypes.length === 0) {
            return new ctor();
        }

        // If it depends on something else, check if there is a dependency loop
        let newFollower;
        if (Injector.checkDependencyLoop) {
            if (follwer && follwer.indexOf(ctor) != -1) {
                throw new Error("Found dependency loop while trying to instantiate " + ctor.name + ". Please check your code.");
            }
            // Prepare for next dependency loop check
            newFollower = (<Function[]>[ctor]).concat(follwer);
        }

        // Instantiate the arguments it's constructor needs
        let args = [];
        for (let i = 0; i < argtypes.length; i++) {
            args.push(Injector.getWithFollower(argtypes[i], newFollower));
        }

        // newIt it
        return new ctor(...args);
    }

    static registerInjectable<T>(ctor: Constructor<T>): Constructor<T> {
        ctor[Injector.isInjectable] = true;
        return ctor;
    }

    static registerSingleton<T>(ctor: Constructor<T>): Constructor<T> {
        ctor[Injector.isInjectable] = true;
        ctor[Injector.isSingleton] = true;
        return ctor;
    }

    static injectDecoratorFactor(undefOrTypeFunc?: {<T>(): Constructor<T>}) {
        if (undefOrTypeFunc) {
            return (target, propertyKey) => {
                Object.defineProperty(target, propertyKey, {
                    get: () => {
                        let obj = Injector.get(undefOrTypeFunc());
                        Object.defineProperty(target, propertyKey, obj);
                        return obj;
                    }
                })
            };
        }
        return (target, propertyKey) => {
            Object.defineProperty(target, propertyKey, {
                get: () => {
                    let obj = Injector.get(Reflect.getMetadata("design:type", target, propertyKey));
                    Object.defineProperty(target, propertyKey, obj);
                    return obj;
                }
            })
        }
    }

    static factorOfDecoratorFactor<T>(ctor: Constructor<T>): MethodDecorator {
        return <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>): TypedPropertyDescriptor<T> | void => {
            ctor[Injector.factorSymbol] = target[propertyKey];
        }
    }

    private static factorSymbol = Symbol('Injector.factorSymbol');
    private static isInjectable = Symbol('Injector.isInjectable');
    private static isSingleton = Symbol('Injector.isSingleton');
    private static singletonInstance = Symbol('Injector.singletonInstance');
    public static checkDependencyLoop = false; // for performance considering, disable it by default.
    public static strictDecorationCheck = true;

}

/* \brief @Injectable() marks a class injectable
 *
 * Use Injector.get() to get a instance of that class
 * Calling Injector.get() multiple times on a @Injectable() decorated class
 * will produce different instances.
 *
 */
export function Injectable(): ClassDecorator {
    return Injector.registerInjectable;
}

/* @Singleton() marks a singleton class.
 * Use Injector.get() to get the instance of singleton class
 */
export function Singleton(): ClassDecorator {
    return Injector.registerSingleton;
}

/* \brief @Inject() marks a property as injected property
 *
 * Unlike @Injectable() unable to instantiate class having dependency loop,
 * @Inject(()=>Type) can instantiate dependencies like A->B->A as long as there is a
 * @Singleton() class among them.
 *
 */
export function Inject(TypeFunc?: {<T>(_?: any): Constructor<T>}): PropertyDecorator {
    return Injector.injectDecoratorFactor(TypeFunc);
}

/* \brief @FactorOf(Class) tell Injector to use it(this decorated function) to create instance of \a Class.
 *
 * Example :
 * \code ts
 * @Singleton()
 * class A {
 *     // it's almost impossible to create A using our Injector.
 *     // Because param val should provided by ourselves but not Injector.
 *     // So a factor function to create it is necessary.
 *    constructor(private val:number){}
 * }
 * class FactorUtils {
 *    @FactorOf(A)
 *    someFunctionToCreateA(){
 *        return {val:1234}
 *    }
 * }
 *
 * Injector.get(A).val === 1234;// this will be true
 * \endcode
 */
export function FactorOf<T>(ctor: Constructor<T>): MethodDecorator {
    return Injector.factorOfDecoratorFactor(ctor);
}
